package it.polimi.jita.cp.block.dd;

import it.polimi.jita.cp.block.Block;
import it.polimi.jita.cp.block.dd.ServiceLevel.ServiceLevelType;
import it.polimi.jita.cp.block.de.DemandEstimator;
import it.polimi.jita.cp.block.de.DemandEstimatorInput;
import it.polimi.jita.cp.block.de.Histogram;
import it.polimi.jita.cp.block.de.Histogram.Bin;
import it.polimi.jita.cp.block.de.IInstantDemandEstimatorData;
import it.polimi.jita.cp.block.scheduler.IVirtualMachineSchedulerData;
import it.polimi.jita.cp.block.scheduler.Scheduler;
import it.polimi.jita.cp.block.scheduler.SchedulerInput;
import it.polimi.jita.cp.block.scheduler.SchedulerOutput;

import java.awt.Color;
import java.awt.GradientPaint;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.csv.CSVRecord;
import org.apache.commons.math3.util.CombinatoricsUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

public class DynamicDecisionMaker extends
		Block<DynamicDecisionMakerInput, DynamicDecisionMakerOutput> {

	private static final DemandEstimator DE = new DemandEstimator();
	private static final Scheduler S = new Scheduler();
	private static final Integer BINS_NUMBER = 100;
	public static final boolean PROBABILISTIC_METHOD = false;
	private static final double PRECISION = 0.001;
	private static final double[] LIMIT = { 0.55, 0.95 };
	private static final boolean SAMPLE_DATA = false;
	private static final boolean NEW_MODEL = true;

	@Override
	protected DynamicDecisionMakerOutput computeOutput() {

		HostData host = getInput().getHostData();
		Collection<VirtualMachineData> vms = host.getVms();
		NewVirtualMachineData newVm = getInput().getNewVm();
		int pCpuNum = host.getpCpuNum();
		Integer samplingTime = getInput().getSamplingTime();

		int logInstantsNum = getInput().getLogInstantsNum();

		List<AbstractVirtualMachineData> allVms = new ArrayList<>();
		allVms.addAll(vms);
		allVms.add(newVm);

		estimate(vms, samplingTime);

		Histogram h = generateDemandHistogram(vms);
		List<Bin> bins = h.getBins();
		newVm.setDemandHistogram(h);

		for (int i = 0; i < logInstantsNum; i++) {
			if (i == 67)
				System.out.println();
			System.out.println(String.format("Progress: %.3g%%",
					((i + 1) * 100d / (double) logInstantsNum)));
			for (VirtualMachineData vm : vms) {
				vm.nextInstant();
			}
			newVm.addInstant();

			for (AbstractVirtualMachineData selectedVm : allVms) {
				selectedVm.setLikelyDemand(false);

				Double slRt = 0d;
				Double slT = 0d;
				while (newVm.hasNextBin()) {
					Bin bin = newVm.nextBin();
					schedule(allVms, pCpuNum);
					double slRtTmp = computeServiceLevel(selectedVm,
							ServiceLevelType.RT, allVms);

					slRt += slRtTmp * bin.getPercentage().doubleValue();
					double slTTmp = computeServiceLevel(selectedVm,
							ServiceLevelType.T, allVms);
					if (slTTmp > 1)
						System.out.println();
					slT += slTTmp * bin.getPercentage().doubleValue();

				}

				// save instant value result of selectedVm
				slT = slT > 1 ? Math.round(slT) : slT;
				slRt = slRt > 1 ? Math.round(slRt) : slRt;

				selectedVm.setServiceLevel(ServiceLevelType.RT, slRt);
				selectedVm.setServiceLevel(ServiceLevelType.T, slT);

				selectedVm.setLikelyDemand(true);
				newVm.resetBinIterator();

				System.out.println(String.format("[i=%s] vm: %s rt=%s t=%s", i,
						selectedVm.getName(), slRt.doubleValue(),
						slT.doubleValue()));
			}

		}

		for (AbstractVirtualMachineData vm : allVms) {
			for (ServiceLevelType slType : ServiceLevelType.values()) {
				System.out.println(String.format("vm %s " + slType
						+ " Asym: %s, " + slType + " Mean: %s, " + slType
						+ " Misc: %s " + slType + " Prob: %s", vm.getName(),
						AbstractVirtualMachineData.asymptoticMean(slType,
								vm.getInstants()), AbstractVirtualMachineData
								.stdMean(slType, vm.getInstants()),
						AbstractVirtualMachineData.miscMean(slType,
								vm.getInstants(), LIMIT[slType.ordinal()]),
						AbstractVirtualMachineData.cdf(slType,
								vm.getInstants(), LIMIT[slType.ordinal()])));
			}
		}

		return new DynamicDecisionMakerOutput(allVms);
	}

	private Histogram generateDemandHistogram(Collection<VirtualMachineData> vms) {
		Histogram histogram = new Histogram(BINS_NUMBER, 0, 1);
		for (VirtualMachineData vm : vms) {
			List<Instant> instants = vm.getCastedInstants();
			for (Instant instant : instants) {
				if (instant.getWait() > 0.01) {
					histogram.incrementBinValue(instant.getRun().doubleValue());
				} else {
					histogram.incrementBinValueGEQ(instant.getRun()
							.doubleValue());
				}
			}
		}

		if (SAMPLE_DATA) {
			int N = histogram.getSum() / vms.size();
			for (int i = 0; i < N; i++) {
				System.out.println(histogram.sampleGEQ(0));
			}
		}

		return histogram;
	}

	private Double computeServiceLevel(AbstractVirtualMachineData selectedVm,
			ServiceLevelType slType, Collection<AbstractVirtualMachineData> vms) {
		switch (slType) {
		case RT:
			Double run;
			Double block;

			// if (selectedVm.isNeedFrequency(PRECISION)) {
			// run = selectedVm.getRunForVCpu();
			// block = 1d - run;
			// } else {
			// Double meanDemand = getMeanDemand(vms);
			// run = getMeanRun(vms);
			// Integer totVCpuNum = getInput().getvCpuNum();
			// Integer totPCpuNum = getInput().getHostData().getpCpuNum();
			// block = getMaxBlock(meanDemand, totVCpuNum, totPCpuNum);
			// for (AbstractVirtualMachineData vm : vms) {
			// if (block > 1d - vm.getRun() / vm.getvCpuNum()) {
			// block = 1d - vm.getRun() / vm.getvCpuNum();
			// }
			// }
			//
			// }
			if (NEW_MODEL) {
				Double meanDemand = getMeanDemand(vms);
				Double vmDemand = selectedVm.getDemandForVCpu();
				if (vmDemand > 1)
					throw new RuntimeException();
				Integer totVCpuNum = getInput().getvCpuNum();
				Integer totPCpuNum = getInput().getHostData().getpCpuNum();
				block = getMaxBlock(meanDemand, totVCpuNum, totPCpuNum,
						vmDemand);
				run = vmDemand + block > 1d ? 1d - block : vmDemand;
				double d = run / (run + block);
				return d;
			} else {
				Double meanDemand = getMeanDemand(vms);
				Integer totVCpuNum = getInput().getvCpuNum();
				Integer totPCpuNum = getInput().getHostData().getpCpuNum();
				block = getMaxBlock(meanDemand, totVCpuNum, totPCpuNum, null);
				run = meanDemand + block > 1d ? 1d - block : meanDemand;
				// return run / (run + block);
				return block;
			}
		case T:
			double slT = selectedVm.getRunForVCpu()
					/ selectedVm.getDemandForVCpu();

			return slT > 1 ? (double) Math.round(slT) : slT;
		default:
			throw new RuntimeException("Service level type is not recognised.");
		}
	}

	private Double getMeanDemand(Collection<AbstractVirtualMachineData> vms) {
		Double meanDemand = 0d;

		for (AbstractVirtualMachineData vm : vms) {
			meanDemand += vm.getDemand();
		}

		return meanDemand / (double) getInput().getvCpuNum();
	}

	private Double getMeanRun(Collection<AbstractVirtualMachineData> vms) {
		Double meanRun = 0d;

		for (AbstractVirtualMachineData vm : vms) {
			meanRun += vm.getRun();
		}

		return meanRun / (double) getInput().getvCpuNum();
	}

	private Double getMaxBlock(Double meanDemand, Integer vCpuNum,
			Integer pCpuNum, Double vmDemand) {
		Integer deltaCpuNum = vCpuNum - pCpuNum;
		if (!(deltaCpuNum > 0))
			return 0d;

		Double maxBlock = 1d - (double) pCpuNum / (double) vCpuNum;

		Double result = 0d;
		for (int i = 0; i < deltaCpuNum; i++) {
			Double c = new Double(CombinatoricsUtils.binomialCoefficient(
					vCpuNum, i));
			Double unused = Math.pow((1 - meanDemand), i);
			Double used = Math.pow(meanDemand, vCpuNum - i);

			result += c * unused * used;

		}
		if (NEW_MODEL) {
			result = vmDemand * (1d / (1d - result) - 1d);
		}

		if (result > maxBlock)
			return maxBlock;

		return result;
	}

	@SuppressWarnings("unchecked")
	private SchedulerOutput schedule(List<AbstractVirtualMachineData> allVms,
			int pCpuNum) {
		SchedulerInput input = new SchedulerInput(pCpuNum);
		input.addAll((List<IVirtualMachineSchedulerData>) (List<?>) allVms);
		S.setInput(input);
		return S.getOutput();
	}

	private void estimate(Collection<VirtualMachineData> vms,
			Integer samplingTime) {
		DemandEstimatorInput input;
		for (VirtualMachineData vm : vms) {
			List<IInstantDemandEstimatorData> instants = vm.getCastedInstants();
			input = new DemandEstimatorInput(instants, samplingTime, null,
					false, vm.getvCpuNum());
			DE.setInput(input);
			DE.getOutput();
		}

	}

	@Override
	protected Set<JFreeChart> createCharts() {
		Set<JFreeChart> charts = new HashSet<>();
		charts.add(createRTChart());
		charts.add(createTChart());
		charts.add(createSummary(ServiceLevelType.RT));
		charts.add(createSummary(ServiceLevelType.T));
		return charts;
	}

	private JFreeChart createSummary(ServiceLevelType slType) {
		// row keys...
		final String series1 = "Linear";
		final String series2 = "Asymptotic";
		final String series3 = "Mixed (" + LIMIT[slType.ordinal()] + ")";
		final String series4 = "P(slRT > " + LIMIT[slType.ordinal()] + ")";

		final DefaultCategoryDataset dataset = new DefaultCategoryDataset();
		for (AbstractVirtualMachineData vm : getOutput().getVms()) {
			Number value = AbstractVirtualMachineData.stdMean(slType,
					vm.getInstants());
			dataset.addValue(value, series1, vm.getName());

			value = AbstractVirtualMachineData.asymptoticMean(slType,
					vm.getInstants());
			dataset.addValue(value, series2, vm.getName());

			value = AbstractVirtualMachineData.miscMean(slType,
					vm.getInstants(), LIMIT[slType.ordinal()]);
			dataset.addValue(value, series3, vm.getName());

			value = AbstractVirtualMachineData.cdf(slType, vm.getInstants(),
					LIMIT[slType.ordinal()]);
			dataset.addValue(value, series4, vm.getName());

		}

		// create the chart...
		final JFreeChart chart = ChartFactory.createBarChart("Comparison of "
				+ slType, // chart
				// title
				"Category", // domain axis label
				"Value", // range axis label
				dataset, // data
				PlotOrientation.VERTICAL, // orientation
				true, // include legend
				true, // tooltips?
				false // URLs?
				);

		// NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART...

		// set the background color for the chart...
		chart.setBackgroundPaint(Color.white);

		// get a reference to the plot for further customisation...
		final CategoryPlot plot = chart.getCategoryPlot();
		plot.setBackgroundPaint(Color.lightGray);
		plot.setDomainGridlinePaint(Color.white);
		plot.setRangeGridlinePaint(Color.white);

		// set the range axis to display integers only...
		final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
		rangeAxis.setRange(0d, 1.05d);
		// rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

		// disable bar outlines...
		final BarRenderer renderer = (BarRenderer) plot.getRenderer();
		renderer.setDrawBarOutline(false);

		// set up gradient paints for series...
		final GradientPaint gp0 = new GradientPaint(0.0f, 0.0f, Color.blue,
				0.0f, 0.0f, Color.lightGray);
		final GradientPaint gp1 = new GradientPaint(0.0f, 0.0f, Color.green,
				0.0f, 0.0f, Color.lightGray);
		final GradientPaint gp2 = new GradientPaint(0.0f, 0.0f, Color.red,
				0.0f, 0.0f, Color.lightGray);
		renderer.setSeriesPaint(0, gp0);
		renderer.setSeriesPaint(1, gp1);
		renderer.setSeriesPaint(2, gp2);

		final CategoryAxis domainAxis = plot.getDomainAxis();
		domainAxis.setCategoryLabelPositions(CategoryLabelPositions
				.createUpRotationLabelPositions(Math.PI / 6.0));
		// OPTIONAL CUSTOMISATION COMPLETED.

		return chart;

	}

	// private JFreeChart createTSummary() {
	// return null;
	// }

	private JFreeChart createRTChart() {
		List<XYSeries> series = new ArrayList<>();
		List<AbstractVirtualMachineData> vms = getOutput().getVms();

		for (AbstractVirtualMachineData vm : vms) {
			XYSeries serie = new XYSeries(vm.getName());
			ListIterator<Instant> instantsIterator = vm.getInstants()
					.listIterator();
			while (instantsIterator.hasNext()) {
				serie.add(instantsIterator.nextIndex(), instantsIterator.next()
						.getSl().get(ServiceLevelType.RT));

			}

			series.add(serie);

		}

		// Add the series to your data set
		XYSeriesCollection dataset = new XYSeriesCollection();
		for (XYSeries serie : series) {
			dataset.addSeries(serie);
		}

		// Generate the graph
		JFreeChart chart = ChartFactory.createXYLineChart(
				"Response Time Index", // Title
				"Time (instants)", // x-axis Label
				"Response Time Index (%)", // y-axis Label
				dataset, // Dataset
				PlotOrientation.VERTICAL, // Plot Orientation
				true, // Show Legend
				true, // Use tooltips
				false // Configure chart to generate URLs?
				);

		chart.getXYPlot().getRangeAxis().setRange(0d, 1.05d);

		return chart;
	}

	private JFreeChart createTChart() {
		List<XYSeries> series = new ArrayList<>();
		List<AbstractVirtualMachineData> vms = getOutput().getVms();

		for (AbstractVirtualMachineData vm : vms) {
			XYSeries serie = new XYSeries(vm.getName());
			ListIterator<Instant> instantsIterator = vm.getInstants()
					.listIterator();
			while (instantsIterator.hasNext()) {
				serie.add(instantsIterator.nextIndex(), instantsIterator.next()
						.getSl().get(ServiceLevelType.T));

			}

			series.add(serie);

		}

		// Add the series to your data set
		XYSeriesCollection dataset = new XYSeriesCollection();
		for (XYSeries serie : series) {
			dataset.addSeries(serie);
		}

		// Generate the graph
		JFreeChart chart = ChartFactory.createXYLineChart(
				"Throughput Time Index", // Title
				"Time (instants)", // x-axis Label
				"Throughput Time Index (%)", // y-axis Label
				dataset, // Dataset
				PlotOrientation.VERTICAL, // Plot Orientation
				true, // Show Legend
				true, // Use tooltips
				false // Configure chart to generate URLs?
				);

		chart.getXYPlot().getRangeAxis().setRange(0d, 1.05d);

		return chart;
	}

	@Override
	protected Map<List<CSVRecord>, CSVRecord> createCSVFiles() {
		Map<List<CSVRecord>, CSVRecord> map = new LinkedHashMap<List<CSVRecord>, CSVRecord>();
		List<AbstractVirtualMachineData> vms = getOutput().getVms();
		Object[] vmNames = new String[vms.size()];

		List<CSVRecord> slRts = new ArrayList<>();
		List<CSVRecord> slTs = new ArrayList<>();
		List<CSVRecord> demands = new ArrayList<>();
		int i = 0;
		for (AbstractVirtualMachineData vm : vms) {
			vmNames[i++] = vm.getName();
		}

		CSVRecord slHeader = createCSVRecord(true, vmNames);

		int instantNumber = vms.get(0).getInstants().size();
		for (int j = 0; j < instantNumber; j++) {
			Object[] slRtRecord = new Double[vms.size()];
			Object[] slTRecord = new Double[vms.size()];
			Object[] demandRecord = new Double[vms.size()];
			int k = 0;
			for (AbstractVirtualMachineData vm : vms) {
				demandRecord[k] = vm.getInstants().get(j).getDemand();
				slTRecord[k] = vm.getInstants().get(j).getSl()
						.get(ServiceLevelType.T);
				slRtRecord[k++] = vm.getInstants().get(j).getSl()
						.get(ServiceLevelType.RT);
			}
			slRts.add(createCSVRecord(false, slRtRecord));
			slTs.add(createCSVRecord(false, slTRecord));
			demands.add(createCSVRecord(false, demandRecord));
		}

		map.put(slRts, slHeader);
		map.put(slTs, slHeader);
		map.put(demands, slHeader);

		return map;
	}

}
